UE5 GAS Learning Notes

Unreal GAS系统使用学习

前置知识

UE网络架构

简介

Gameplay Ability System,简称(GAS)是一个健壮的、高度可扩展的、gameplay框架,通常用于构建RPG、MOBA等游戏的完整战斗逻辑框架。通过GAS,可以快速地制作游戏中的主动/被动技能、各种效果buff、计算属性伤害、处理玩家各种战斗状态逻辑。

GAS试图将机制提取为通用的游戏设计模式,并提供框架来解决常见的Gameplay实现问题,同时使上下文因项目而异。

GAS提供了哪些功能?

  1. 实现了带有消耗和冷却功能的角色技能
  2. 处理数值属性(生命、魔法、攻击力、防御力)
  3. 应用状态效果(击飞、着火、眩晕)
  4. 应用游戏标签(GameplayTags)
  5. 生成特效和音效
  6. 完整的网络复制、预测功能

适合使用GAS的项目

  1. C++项目,开发人员有充足的C++开发经验

  2. 使用Dedicated Server专用服务器的联机游戏(单纯的游戏逻辑服务端,与Dedicated Server相对的是Listen Server,其中一位玩家充当服务器,容易出现“主机优势”)

  3. 项目有大量且复杂的技能逻辑设计需求

GAS的主要优势

  • 网络复制(Network Replication): 不必担心你的属性或减益效果得不到妥善地应用或复制。GAS会为你处理内部逻辑。
  • 模块化(Modularity): 添加或更改游戏机制通常与实现和赋予新技能一样简单。通过将Gameplay功能分解为单独的资产,技能系统可以在完全不同的游戏对象或机制之间提供通用的通信层。例如,生命值(Health)可以划分到自己的属性集,并通过来自各种系统的Gameplay效果进行交互。
  • 快速迭代(Fast iteration): GAS可以轻松更改单个游戏规则,而无需修改整个系统。用于计算游戏的数据源可以轻松交换,并且可以从相应的Gameplay效果中修改动作效果。

我为什么学习GAS

编写样板代码通常易出错且耗时,尤其是对于多人游戏而言。例如,你不希望花费大量时间来确保你的生命(Health)值正确复制,或者在你决定具有相同行为的能量(Energy)值时,复制相同的代码行。

GAS通过提供尽可能实现常见Gameplay功能的基础来解决这些问题,同时保持机制中立。GAS并非强制使用诸如生命值(Health)、弹药(Ammo)、近战攻击(Melee Attack)或毒药减益(Poison Debuff)之类的概念,而是提供了能够定义、复制和使用 属性(Attributes)技能(Abilities)效果(Effects) 的工具,然后你可以将这些工具专用于满足给定Gameplay机制的需求。

于我而言:公司的项目需要使用到联网的游戏服务。所以你会在我的笔记中看到一些斜体的备注,这些备注意味着对我自己项目的备注。可以忽略。

GAS的组件

ASC:who谁能放技能?

Ability System Component(ASC)是整个 GAS 的基础组件。ASC 本质上是一个 UActorComponent,用于处理整个框架下的交互逻辑,包括使用技能(GameplayAbility)、包含属性(AttributeSet)、处理各种效果(GameplayEffect)。所有需要应用 GAS 的对象(Actor),都必须拥有 GAS 组件。(即所有有技能交互的角色都是ASC)。

  • ASC是一种角色组件,负责和GA、GE、AS打交道。
  • 一般只放在Character or PlayerState上。
  • 拥有 ASC 的 Actor 被称为 ASC 的 OwnerActor,ASC 实际作用的 Actor 叫做 AvatarActor。

GA:How:技能的逻辑?

Gameplay Ability(GA)标识了游戏中一个对象(Actor)可以做的行为或技能。它代表角色可以执行的任何技能或行为。
能力(Ability)可以是普通攻击或者吟唱技能,可以是角色被击飞倒地,还可以是使用某种道具,交互某个物件,甚至跳跃、飞行等角色行为也可以是Ability。
Ability可以被赋予对象或从对象的ASC中移除,对象同时可以激活多个GameplayAbility。

  • 基本的移动输入、UI交互行为则不能或不建议通过GA来实现。
  • 通过蓝图继承GamePlay Ability来实现

GA的执行流程

graph TD A[TryActivateAbility] --> B{CanActivateAbility} B -->|Yes| C[ActivateAbility] B -->|No| D[EndAbility] C --> E[Start AbilityTask] E -->|Some Time Later| F[CommitAbility] F -->|Can't Afford| D F --> G[Start AbilityTask] G -->|Some Time Later| H[Apply GameplayEffects] G --> I[Start AbilityTask] I -->|Some Time Later| J[Apply GameplayEffects] I -->|Some Time Later| K[End Ability] D --> L[OnEndAbility] K --> L L --> M[Cleanup]
  1. TryActivateAbility :首先判断一下能不能执行。
  2. ActivateAbility:执行一套技能(有技能动画,比如发射一个火焰)。
  3. CommitAbility:技能释放了成功了,要扣蓝。
  4. Start AbilityTask * 2:技能执行中…技能执行结束。然后应用对应效果。

其中ActivateAbility和End Ability是类似于BeginPlay和EndPlay这样的函数,在蓝图中调用。

蓝图中的设置

主要选项解释

常见蓝图
  • EndAbility 结束这个能力
高级设置
image-20251028173049722
  • Replication Policy(复制策略): 决定该Ability的激活、执行等信息是否在网络上传播。常见选项有:

    • Not Replicated:仅本地执行,不同步到其他客户端/服务器。
    • Replicated:在服务器和客户端之间同步。
  • Instancing Policy(实例化策略): 决定Ability的实例化方式。

    • Non-Instanced:所有激活者共享同一个Ability对象,适合无状态逻辑。
    • Instanced Per Actor:每个拥有者有独立实例,适合有状态逻辑。
    • Instanced Per Execution:每次激活都新建实例,适合需要隔离状态的复杂技能。
  • Server Respects Remote Ability Cancellation(服务器尊重远端取消):勾选后,服务器会响应客户端请求取消Ability的操作。适合需要客户端主动取消技能的场景。

  • Retrigger Instanced Ability(可重复触发实例化Ability):允许Ability在已激活时再次被激活(会重新实例化),适合可叠加或可重复释放的技能。

  • Net Execution Policy(网络执行策略):决定Ability的激活和执行在哪端进行。

    • Local Predicted:客户端预测执行,服务器校验。
    • Local Only:仅本地执行。
    • Server Only:仅服务器执行。
    • Server Initiated:服务器发起,客户端可参与。
  • Net Security Policy(网络安全策略):控制Ability的安全等级,防止被恶意客户端滥用。

    • Client Or Server:客户端和服务器都可激活。
    • Server Only Execution:只有服务器可激活。
    • Server Only Execution and Data:只有服务器可激活且数据仅在服务器。

GA要做的事情

image-20251028200009953

一般GA要做的事有:

  • 设置GA的Tag、CD、Cost等属性。
  • 获取必要信息,主要通过Get Actor Info。如果是通过Event调用的GA(使用Activate Ability From Event节点作为输入),还可以通过Gameplay Event Data获取。
  • 编写逻辑,如播放动画、应用GE、应用冲量等。
  • 一定不要忘了EndAbility。

GA的调用

GA的调用分成了主动调用(释放技能)和被动调用(挨打)两类,下面依次介绍不同的调用方法。

主动调用

在蓝图中主要有by Class和by Tag 两种调用方法。

img

byClass一次只能Activate一个GA,byTag可以Activate任意多个GA,配合Tag容器使用。

如果使用EnhancedInputAction插件来管理输入,要注意在某些设置下trigger会每帧都进行输出(本人测试环境为4.27,UE5似乎有一些改动。
1.62GB
古代山谷项目就使用了新版输入插件和GAS系统,可以看一看实现方法)。

只要能获取ASC,就可以在任何地方调用GA,比如行为树Task蓝图,甚至在GA蓝图中调用其他GA。

被动调用

Trigger可以理解为一个Tag,当ASC组件收到一个Trigger时,就会自动调用所有拥有该Trigger的GA。

Trigger的Tag在GA的details面板中设置。

img

Trigger的触发方式有三种,分别是:

  • Gameplay Event: 当Owner收到一个带有Tag的Gameplay Event(不是Gameplay Effect的GE!)时调用一次GA,此时Owner不会拥有对应的Tag
  • Owner Tag Added: 当Owner获取对应Tag的时候调用一次GA。
  • Owner Tag Present: 当Owner拥有Tag时调用GA,失去Tag时移除。

一般使用第一种方法,并配合SendGameplayEventToActor节点使用,如下图所示。(这张图是很久以前截的,Tag建议以Event开头)

img

受击效果的例子,发送一个Tag为Hit的Event给碰撞检测到的Actor

使用Gameplay Event调用的好处是,可以传入数据(Payload),是除了Get Actor Info外的另一种信息传递方法。

此时应该删除ActiveAbility节点,转而使用ActivateAbilityFromEvent事件。(不要通过在左上角重载函数的方式,右键空白处搜索才是对的

GE:What技能改变的属性?

Gameplay Effect(GE)是Ability对自己或他人产生影响的途径
GE通常可以被理解为我们游戏中的buff。比如增益/减益效果(修改属性)。
但是GAS中的GE也更加广义,释放技能时候的伤害结算,施加特殊效果的控制、霸体效果(修改GameplayTag)都是通过GE来实现的。

  • GE只是一个可配置的*数据表 *,不可以添加逻辑。开发者创建一个UGameplayEffect的派生蓝图,就可以根据需求制作想要的效果。
  • GE是纯蓝图

GT:if技能改变的条件?

FGameplayTags是一种层级标签,如Parent.Child.GrandChild。
通过GameplayTagManager进行注册。
替代了原来的Bool,或Enum的结构,可以在玩法设计中更高效的标记对象的行为或状态(比如一个灼烧效果,GE完成之后,便可以取消标签)。
我理解是一个在Actor下的Json。

  • Tag的层级关系也需要合理设计,到了后期修改成本比较大。

Attribute Set

负责定义和持有属性,并且管理属性的变化,包括网络同步
需要在Actor中被添加为成员变量,并注册到ASC(C++)。
一个ASC可以拥有一个或多个(不同的)AttributeSet,因此可以角色共享一个很大的Attribute Set,也可以每个角色按需添加Attribute Set。
可以在属性变化前(PreAttributeChange)后(PostGameplayEffectExecute)处理相关逻辑,可以通过委托的方式绑定属性变化。

Player State

Player State 架构示意图
PlayerState 是 Unreal Engine 中用于存储玩家相关信息的特殊 Actor 类。在多人游戏中,PlayerState 会自动在服务器和所有客户端之间复制,非常适合存储需要跨网络同步的玩家数据。

在 GAS 系统中,ASC 可以选择挂载在 PlayerState 上而不是 Character 上。这样做的好处是:

  • 持久性:当角色死亡或重生时,PlayerState 依然存在,技能和属性数据不会丢失
  • 网络复制:PlayerState 天然支持网络复制,适合多人游戏场景
  • UI 访问:UI 可以更方便地访问玩家的技能和属性信息,无需依赖具体的 Character 实例
  • 观察者模式:即使玩家处于观察者状态(没有 Character),也能保持技能系统的状态

常见的做法是:

  • 玩家控制的角色:ASC 放在 PlayerState 上
  • AI 控制的角色:ASC 直接放在 Character 上

组件总结

Visual:技能的视觉效果?GameplayCue
Async:技能的长时行动?(异步)GameplayTask
Send:技能的消息事件?GamePplayEvent
这俩是同一个东西

基础使用

开启插件

image-20251027203033649 image-20251027203540226
  1. C++项目,安装Gameplay Ability插件。
  2. 在Tools菜单下面有GameplayCue Editor有了,就说明开启成功了。
  3. 在新建的蓝图的页面可以看到GamePlay相关的东西

使用Rider快速添加Unreal类

image-20251027215401030

通过这里创建一个Character。可以快速拥有一些头文件。

ASC组件架构

classDiagram class ACharacter class ARPGCharacterBase { +AbilitySystemComponent } class BP_Character { +公共逻辑 } class BP_PlayerCharacter { -组装Camera等组件 -输入绑定 } class BP_NPCCharacter ACharacter <|-- ARPGCharacterBase ARPGCharacterBase <|-- BP_Character BP_Character <|-- BP_PlayerCharacter BP_Character <|-- BP_NPCCharacter

你需要自己创建一个继承自ACharacter的C++类。

之后想用蓝图,就从这个自定义的C++Character类派生就可以了。

写一个Base GAS ACharacter文件

1
2
3
4
5
PublicDependencyModuleNames.AddRange(new string[]
{
"Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput",
"GameplayAbilities","GameplayTags","GameplayTasks"
});

在Build文件里添加如下的内容GameplayAbilities、GameplayTags、GameplayTasks。

在头文件里面添加一个AbilitySystemComponent,直接在角色上挂载 AbilitySystemComponent(ASC),可以适配多人游戏。

1
TObjectPtr<UTCGAbilitySystemComponent> AbilitySystemComponent;

在.cpp中构造函数部分实例化AbilitySystemComponent(ASC)。

1
2
//实例化ASC
AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystem"));

角色类继承IAbilitySystemInterface接口,并实现GetASC函数

1
2
3
4
5
#include "AbilitySystemInterface.h"
class ARPG_UNREAL_API ACharacterBase : public ACharacter, public IAbilitySystemInterface

public:
UAbilitySystemComponent* GetAbilitySystemComponent()const override;

在CPP中实现

1
2
3
4
UAbilitySystemComponent* ACharacterBase::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}

👆这里的return即使写为null,也可能可能调用

这个在项目中的路径在:Source/TCG_AwesomeLive/CharacterSystem/TCGCharacterBase.h

把GA数组加入到Actor中

可以将UE原生的UGameplayAbility直接加入到需要Ability的Actor中(也是加到这个Base GAS ACharacter文件中)

1
2
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Test|Abilities")
TArray<TSubclassOf<UGameplayAbility>> CharacterAbilities;

在C++中赋予GA:我们可以将我们所有的技能储存在一张DA表之中,然后从DA表里读取数据,并使用AbilitySystemComponent->GiveAbility将数据赋予ASC。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 添加能力
for(FTCGAblityInfo& Info : AllAbilitiesInfo){
if (Info.AbilityClass == nullptr)
continue;
CharacterAbilities.Add(Info.AbilityClass);
}

for (auto StartupAbility : CharacterAbilities)
{
AbilitySystemComponent->GiveAbility(
FGameplayAbilitySpec(StartupAbility, 1, INDEX_NONE, this)
);
}

这样我们就可以在继承了Base Character的子类中,看到Ability的选项了。

image-20251028184812916
赋予GA的方式有很多,比如在需要的蓝图中调用Give Ability节点。还可以使用GE赋予GA。

在官方的Lyra案例里面使用AbilitySet来储存一类Ability,通过加载来实现赋予。参看下文Unreal官方案例“赋予GA”部分。

Unreal官方案例

UE中有一个非常不错的官方示例Lyra里面展示了一个完整的GAS系统。

项目里的设置

image-20251111183652281

这个项目里开发者设置了一些调整选项,可以设定一些debug的参数。

比如子弹射击停留时间,生成的bot的数量。

项目蓝图常见前缀标识

WID_
全称可理解为 Weapon Item Definition。
用途:武器类的“物品定义”蓝图资产,继承自 ULyraInventoryItemDefinition(你在代码里看到的基类)。
ID_
可理解为(Generic)Item Definition。
用途:非武器的一般物品定义(比如消耗品、弹药、包裹、可堆叠资源等)也继承 ULyraInventoryItemDefinition,用 ID_ 前缀区分与 WID_(武器专用)资产。
B_
B_ 前缀在 Lyra 中通常代表一个 Blueprint 类资产(等同很多团队常用的 BP_,Lyra 项目里取了更短的 B_)。

源码解析

  • 这一部分我主要参考的是这里加上了一些我自己的补充。整体结构采用了原本的结构,但是有我自己的删减,可以对照地看。

初始化Ability System

创建ASC

ASC用于总体控制GAS功能,可以挂在Character上或者PlayerState上,对于角色会死亡重生的场景,挂在PlayerState上更合适,不会因Character销毁而丢失数据。因此Lyra选择把ASC挂在了PlayerState上,这样还有额外作用,就是按Tab键显示计分板这种战斗无关的功能,也可以用GA实现,以往的习惯都是硬编码。

ULyraAbilitySystemComponent本身是一个Manager,与数据无关,所以PlayerState直接New了一个对象即可:

Source/LyraGame/GameModes/LyraGameState.h/cpp

1
2
3
4
5
6
7
8
//声明
UPROPERTY(VisibleAnywhere, Category = "Lyra|PlayerState")
TObjectPtr<ULyraAbilitySystemComponent> AbilitySystemComponent;

//创建
AbilitySystemComponent = ObjectInitializer.CreateDefaultSubobject<ULyraAbilitySystemComponent>(this, TEXT("AbilitySystemComponent"));
AbilitySystemComponent->SetIsReplicated(true);
AbilitySystemComponent->SetReplicationMode(EGameplayEffectReplicationMode::Mixed);
ASC里面也会记录Actor

Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemComponent.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private:

/** The actor that owns this component logically */
UPROPERTY(ReplicatedUsing = OnRep_OwningActor)
TObjectPtr<AActor> OwnerActor;

/** The actor that is the physical representation used for abilities. Can be NULL */
UPROPERTY(ReplicatedUsing = OnRep_OwningActor)
TObjectPtr<AActor> AvatarActor;

public:

/** Cached off data about the owning actor that abilities will need to frequently access (movement component, mesh component, anim instance, etc) */
TSharedPtr<FGameplayAbilityActorInfo> AbilityActorInfo;

ASC也会记录关联Actor的信息:

OwnerActor挂在哪个Actor上,这里是PlayerState;

AvatarActor是使用Ability实体,这里是Character。

这两个属性是ASC经常要获取的变量,除此之外,还有多个与Actor关联的属性会被高频读取,为了提高读取性能,ASC索性把它们复制了一份,集中存储在AbilityActorInfo变量中,包括了PlayerController,SkeletalMeshComponent,AnimInstance,MovementComponent,AffectedAnimInstanceTag。

赋予GA(前建立Experience做数据衔接)

创建完ASC后,要往这个容器里赋予GA了,表示Actor有这些能力。

Lyra工程高度模块化,配置比较分散,使用AbilitySet来储存一类Ability,通过加载来实现自动化配置和赋予GA。

首先创建一个Experience,首先什么是Experience?

  • 定义角色: Experience(通过 ULyraExperienceDefinition)把一组运行时行为、资源和规则打包成一个可切换的体验(例如不同模式、地图或玩法配置)。

  • 主要用途:在游戏启动或切换体验时告诉引擎“要启用哪些 GameFeatures、默认的 Pawn 数据、以及需要执行哪些动作/赋能”

  • 原因

    • 统一配置与可切换性:把调表(技能、效果、默认 pawn 等)放到 Experience 里,便于在不同玩法/模式间切换和复用,不用把能力写死在代码里。
    • 数据驱动与延迟加载:Experience 可以在运行时启/停 GameFeatures、注入资源,再把需要的能力/效果批量下发。
    • 作用域与生命周期可控:通过 Experience 可以在启用时授予、停用时撤销,方便整体管理和热加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UCLASS(BlueprintType, Const)
class ULyraExperienceDefinition : public UPrimaryDataAsset
{
...
public:
UPROPERTY(EditDefaultsOnly, Category = Gameplay)
TArray<FString> GameFeaturesToEnable;

UPROPERTY(EditDefaultsOnly, Category=Gameplay)
TObjectPtr<const ULyraPawnData> DefaultPawnData;

UPROPERTY(EditDefaultsOnly, Instanced, Category="Actions")
TArray<TObjectPtr<UGameFeatureAction>> Actions;

UPROPERTY(EditDefaultsOnly, Category=Gameplay)
TArray<TObjectPtr<ULyraExperienceActionSet>> ActionSets;
};

如图Lyra的结构。

image-20251111172739503

继承自ULyraExperienceDefinition的:B_BasicShooterTestB_BasicShooterTest这个数据类里的Action分组里面,有需要授予的GA的DA表。

比如:**AbilitySet_Elimination(下面那个) 和 HeroData_ShooterGame里面的AbilitySet_**ShooterHero。

  • 首先看AbilitySet_Elimination DA表(它继承自LyraAbilitySet)。
image-20251111171556976

打开这个AbilitySet_Elimination,里面包含两个GA。
GA_ShowLeaderboard_TDM用于按Tab显示计分板。
GA_AutoRespawn用于死亡复活,这两个GA与Character战斗无关,在没用Character时也能执行。

  • 然后看AbilitySet_ShooterPistol DA表(储存在总表的HeroData_ShooterGame里面 它继承自ULyraPawnData)
image-20251111174500121
AbilitySet_ShooterHero

包含Character相关的GA,数量较多。比如GA_Hero_Jump实现了跳跃,GA_Emote实现了跳舞。

  • 然后还有一个AbilitySet_ShooterPisto
image-20251111181356512

它不是定义在B_BasicShooterTestB_BasicShooterTest这张大表里面的。

而是通过WID_Pistol被具体的蓝图调用。

然后对每个GA,调用UAbilitySystemComponent::GiveAbility函数进行赋予。

image-20251111185235151

会在很多地方调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FGameplayAbilitySpecHandle UAbilitySystemComponent::GiveAbility(const FGameplayAbilitySpec& Spec)
{
ABILITYLIST_SCOPE_LOCK();
FGameplayAbilitySpec& OwnedSpec = ActivatableAbilities.Items[ActivatableAbilities.Items.Add(Spec)];

if (OwnedSpec.Ability->GetInstancingPolicy() == EGameplayAbilityInstancingPolicy::InstancedPerActor)
{
// Create the instance at creation time
CreateNewInstanceOfAbility(OwnedSpec, Spec.Ability);
}

OnGiveAbility(OwnedSpec);
MarkAbilitySpecDirty(OwnedSpec, true);

UE_LOG(LogAbilitySystem, Log, TEXT("%s: GiveAbility %s [%s] Level: %d Source: %s"), *GetNameSafe(GetOwner()), *GetNameSafe(Spec.Ability), *Spec.Handle.ToString(), Spec.Level, *GetNameSafe(Spec.SourceObject.Get()));
UE_VLOG(GetOwner(), VLogAbilitySystem, Log, TEXT("GiveAbility %s [%s] Level: %d Source: %s"), *GetNameSafe(Spec.Ability), *Spec.Handle.ToString(), Spec.Level, *GetNameSafe(Spec.SourceObject.Get()));
return OwnedSpec.Handle;
}

函数参数为FGameplayAbilitySpec,表示“这次赋予”的描述信息。

不仅包含这次赋予的描述信息,比如GA的Class,指定GA的等级,GA绑定的输入Tag。而且也是GA在ASC上的runtime表示,有很多runtime信息。每个Spec都有唯一的ID,称为Handle,通过自增赋予,各系统间Spec的传递都通过Handle实现。

值得注意的是Spec.SourceObject属性,表示这个GA所关联的最“密切”的Object,在创建时由我们指定。
比如GA_Weapon_Fire_Pistol,是枪开火的GA,SourceObject就是武器B_WeaponInstance_Pistol;
GA_Hero_Jump是角色跳跃的GA,SourceObejct就是Character;
GA_ShowLeaderboard_TDM则是与战斗无关的打开计分板的GA,OurceObject就是PlayerStat。

1
2
3
4
5
6
7
8
9
10
11
12
13
USTRUCT(BlueprintType)
struct GAMEPLAYABILITIES_API FGameplayAbilitySpec : public FFastArraySerializerItem
{
GENERATED_USTRUCT_BODY()

...

UPROPERTY(NotReplicated)
TArray<TObjectPtr<UGameplayAbility>> NonReplicatedInstances;

UPROPERTY()
TArray<TObjectPtr<UGameplayAbility>> ReplicatedInstances;
}

添加时,首先把Spec加入到ActivatableAbilities数组中,然后根据Policy创建一个GA的实例,默认的InstancedPerActor是要创建的。创建完成后,如果GA设置为网络同步,就加入到Spec.ReplicatedInstances中,否则加入Spec.NonReplicatedInstances。

注册Trigger

GA可以绑定一些Trigger,实际是Tag,当发出GamePlayEvent,并指定对应Tag后,就能激活GA了。

所以这里的Tag为了更方便做判断具体的GA。

  • 首先是InputTag,它本身非GAS框架内的,是Lyra自己做的一套输入和GA的映射。InputTag在AbilitySet中指定

image-20251111191635777AbilitySet_ShooterPisto

image-20251111191708052

AbilitySet_ShooterPistol

image-20251111191718509

AbilitySet_ShooterPisto

创建Spec时会把InputTag添加到其DynamicSpecSourceTags,DynamicSpecSourceTags是一个通用Tag容器。
该Tags在结构体FGameplayAbilitySpec里面,也就是说,一个FGameplayAbilitySpec代表的是传递的一个Ability的能力,然后里面有很多DynamicAbilityTags

注册的路径如下:

image-20251111205815528
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void ULyraAbilitySet::GiveToAbilitySystem(ULyraAbilitySystemComponent* LyraASC, FLyraAbilitySet_GrantedHandles* OutGrantedHandles, UObject* SourceObject) const
{
...

// Grant the attribute sets.
for (int32 SetIndex = 0; SetIndex < GrantedAttributes.Num(); ++SetIndex)
{
...
}

// Grant the gameplay abilities.
for (int32 AbilityIndex = 0; AbilityIndex < GrantedGameplayAbilities.Num(); ++AbilityIndex)
{
const FLyraAbilitySet_GameplayAbility& AbilityToGrant = GrantedGameplayAbilities[AbilityIndex];

if (!IsValid(AbilityToGrant.Ability))
{
UE_LOG(LogLyraAbilitySystem, Error, TEXT("GrantedGameplayAbilities[%d] on ability set [%s] is not valid."), AbilityIndex, *GetNameSafe(this));
continue;
}

ULyraGameplayAbility* AbilityCDO = AbilityToGrant.Ability->GetDefaultObject<ULyraGameplayAbility>();

FGameplayAbilitySpec AbilitySpec(AbilityCDO, AbilityToGrant.AbilityLevel);
AbilitySpec.SourceObject = SourceObject;
AbilitySpec.GetDynamicSpecSourceTags().AddTag(AbilityToGrant.InputTag);

const FGameplayAbilitySpecHandle AbilitySpecHandle = LyraASC->GiveAbility(AbilitySpec);

if (OutGrantedHandles)
{
OutGrantedHandles->AddAbilitySpecHandle(AbilitySpecHandle);
}
}

// Grant the gameplay effects.
for (int32 EffectIndex = 0; EffectIndex < GrantedGameplayEffects.Num(); ++EffectIndex)
{
...
}
}

在授予ASC Ability的时候,将Tags Effect等信息一并传入。

授予GA的逻辑,以上就是服务器初始化ASC并赋予GA的全部过程。

image-20251111210326498

同步客户端

ASC

首先,ASC作为一个Component挂在PlayerState上,是可以同步的。

由于AvatarActor是Character,因此OwnerActor和AvatarActor的最终设置,需要客户端的Character创建后才能执行。

具体为Character创建后,触发LyraHeroComponent的加载资源流程,当加载完成并且客户端获取到PlayerState后,就把当前阶段设置为DataAvailabe,并最终设置OwnerActor和AvatarActor。

GASpec

Spec存于ActivatableAbilities容器中,本身是FastArray,标记了Replicated,可正常网络同步

1
2
UPROPERTY(ReplicatedUsing = OnRep_ActivateAbilities, BlueprintReadOnly, Transient, Category = "Abilities")
FGameplayAbilitySpecContainer ActivatableAbilities;

Spec同步到客户端后,FastArray会触发FGameplayAbilitySpec::PostReplicatedAdd()方法,里面再和服务器同样的UAbilitySystemComponent::OnGiveAbility()方法来注册Spec。

1
2
3
4
5
6
7
8
void FGameplayAbilitySpec::PostReplicatedAdd(const struct FGameplayAbilitySpecContainer& InArraySerializer)
{
if (InArraySerializer.Owner)
{
//...
InArraySerializer.Owner->OnGiveAbility(*this);
}
}

对于不同步但InstancedPerActor的GA,会在此时创建实例,加入到NonReplicatedInstances数组,然后再注册AbilityTriggers。

Spec的GA实例

首先,GA实例只同步给Autonomous客户端,不会同步给SimulateProxy。然后,会把GA实例作为ASC的SubObject进行同步,这是SubObject同步是UE自身支持的机制。

主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//同步GA
void UAbilitySystemComponent::AddReplicatedInstancedAbility(UGameplayAbility* GameplayAbility)
{
//...
const ELifetimeCondition LifetimeCondition = bReplicateAbilitiesToSimulatedProxies ? COND_None : COND_ReplayOrOwner;
AddReplicatedSubObject(GameplayAbility, LifetimeCondition);
}
//SubObject同步
void UActorComponent::AddReplicatedSubObject(UObject* SubObject, ELifetimeCondition NetCondition)
{
if (AActor* MyOwner=GetOwner())
{
MyOwner->AddActorComponentReplicatedSubObject(this, SubObject, NetCondition);
}
}

同步到客户端后表现如下:

img

常用调整命令行

ShowDebug AbilitySystem 展示Ability相关的内容

踩坑记录

GAS网络生成的BP需要考虑所有者

image-20251114160307855image-20251114160401410

如果是在Server端生成的BP,一定要指定“Owner”,它意味着是这个生成的东西在各网络设备之间指定的所有者。
当这个所有者内部需要执行一个网络的Event的时候,它需要通知一个客户端,如果没有Owner,这个Event会被拒绝执行。

随机数问题

注意网络端和服务端生成随机数会不一样。

在GE里拿到“事件触发者”和“事件被影响者”

image-20251120172105945

如上图,我有一个需求,投掷物。
需要给GE传递“投掷物这个物体”和“被撞的人”。
其中,在我这里,“被撞的人”是要执行的ASC的所有者(即这里的Instigator),在应用GE的时候必须提供。

调用 BP_ApplyGameplayEffectToTarget 时,它内部会:

  • 用「作为 Target 的 ASC 的 Owner / Avatar Actor」来构造 FGameplayEffectContextHandle;
  • 在这个 EffectContext 里自动填好 Instigator、EffectCauser 等字段;
  • 把这个 EffectContext 放进 FGameplayEffectSpec,最后应用到 Target;

(正如图上所示,它其实有两个Target节点,上面那个是调用这个事件的Target(蓝图中常见),下面那个是被命中的“被撞的人”,因为碰巧都叫Target所以在这里重复了)

这个比较好理解。但,实际上GC一个内部结构里,有三个储存用来表达“事件责任相关人”的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
USTRUCT(BlueprintType, meta = (HasNativeBreak = "/Script/GameplayAbilities.AbilitySystemBlueprintLibrary.BreakGameplayCueParameters", HasNativeMake = "/Script/GameplayAbilities.AbilitySystemBlueprintLibrary.MakeGameplayCueParameters"))
struct FGameplayCueParameters
{
GENERATED_USTRUCT_BODY()

/** Instigator actor, the actor that owns the ability system component */
UPROPERTY(BlueprintReadWrite, Category=GameplayCue)
TWeakObjectPtr<AActor> Instigator;

/** The physical actor that actually did the damage, can be a weapon or projectile */
UPROPERTY(BlueprintReadWrite, Category=GameplayCue)
TWeakObjectPtr<AActor> EffectCauser;

/** Object this effect was created from, can be an actor or static object. Useful to bind an effect to a gameplay object */
UPROPERTY(BlueprintReadWrite, Category=GameplayCue)
TWeakObjectPtr<const UObject> SourceObject;
}

在文件UE_5.6\Engine\Plugins\Runtime\GameplayAbilities\Source\GameplayAbilities\Public\GameplayEffectTypes.h中

这三个分别是:Instigator、EffectCauser、SourceObject。

按照说明,他们“责任”分别的作用是

  • Instigator:谁是「施加这个效果的控制者」,通常是 Pawn/Character。
  • EffectCauser:是哪一个 Actor/Component「具体造成了这次效果」,比如武器、投掷物。
  • SourceObject:一个完全开放给你用的 UObject*,可以是武器实例、技能实例、DataAsset、投掷物等。

这三个字段的值,不一定都有,是否自动填,取决于你走的 GAS 调用路径。通常应用的路径上,Instigator和EffectCauser都是同一个值,都是这个ASC的拥有者。但是SourceObject通常是空的(通过GA主动和触发的GE有机会把GA的SourceObejct传入进去),所以可以自己指定,用作自己的参数。

参考资料

  1. 官方教学视频:我看着这个视频作为最初的入门

  2. 知乎GAS系统快速入门

  3. Unreal官方的GAS讲解

  4. 关于GA的讲解:别忘了可以点开B站的AI翻译

  5. 一个很不错的关于GAS系统实操讲解

  6. 一个很不错的Lyra示例的讲解

  7. 文字讲解Lyra结构适合作为研究基础,然后配合上面的视频看。